<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>纯 HTML 烟花效果</title>
<style>
  html,body{height:100%;margin:0;background:#000;overflow:hidden}
  canvas{display:block; width:100%; height:100%}
  .hint {
    position: absolute;
    left: 12px;
    top: 12px;
    color: rgba(255,255,255,0.85);
    font-family: system-ui, "Microsoft Yahei", Arial;
    font-size: 13px;
    user-select:none;
  }
</style>
</head>
<body>
<div class="hint">点击或触摸任意位置触发烟花 • 自动随机烟花已开启</div>
<canvas id="c"></canvas>
<script>
(() => {
  const canvas = document.getElementById('c');
  const ctx = canvas.getContext('2d', { alpha: true });
  let W = canvas.width = innerWidth;
  let H = canvas.height = innerHeight;

  window.addEventListener('resize', () => {
    W = canvas.width = innerWidth;
    H = canvas.height = innerHeight;
  });

  // 参数
  const GRAVITY = 0.06;
  const FRICTION = 0.995;
  const AUTO_INTERVAL = 900; // 自动烟花间隔(ms)
  const MAX_PARTICLES = 1000;

  // 辅助
  function rand(min, max) { return Math.random() * (max - min) + min; }
  function randInt(min, max) { return Math.floor(rand(min, max)); }
  function hsl(h, s=100, l=60) { return `hsl(${h},${s}%,${l}%)`; }

  // 火箭：由底部上升后爆炸成多个粒子
  class Rocket {
    constructor(x, y, vx, vy, hue) {
      this.x = x; this.y = y; this.vx = vx; this.vy = vy;
      this.hue = hue;
      this.exploded = false;
      this.trail = [];
      this.maxTrail = 6;
    }
    update() {
      // 记录尾迹
      this.trail.unshift({x: this.x, y: this.y});
      if (this.trail.length > this.maxTrail) this.trail.pop();

      this.vy += -GRAVITY * 0.4; // 上升受重力相反（火箭往上）
      this.vx *= 0.998;
      this.x += this.vx;
      this.y += this.vy;

      // 条件触发爆炸（随机或达到高度）
      if (this.vy >= -0.5 || Math.random() < 0.01 || this.y < H * 0.25) {
        this.exploded = true;
        explode(this.x, this.y, this.hue);
      }
    }
    draw(ctx) {
      // 绘制尾迹
      for (let i = 0; i < this.trail.length; i++) {
        const p = this.trail[i];
        const a = (1 - i / this.trail.length) * 0.9;
        ctx.beginPath();
        ctx.globalAlpha = a;
        ctx.fillStyle = hsl(this.hue, 100, 60);
        ctx.arc(p.x, p.y, 2 + i*0.6, 0, Math.PI*2);
        ctx.fill();
      }
      // 火箭头
      ctx.beginPath();
      ctx.globalAlpha = 1;
      ctx.fillStyle = hsl(this.hue, 100, 60);
      ctx.arc(this.x, this.y, 3.5, 0, Math.PI*2);
      ctx.fill();
    }
  }

  // 烟花粒子
  class Particle {
    constructor(x, y, vx, vy, hue, size, life) {
      this.x = x; this.y = y;
      this.vx = vx; this.vy = vy;
      this.hue = hue;
      this.size = size;
      this.life = life;
      this.age = 0;
      this.alpha = 1;
    }
    update() {
      this.vx *= FRICTION;
      this.vy *= FRICTION;
      this.vy += GRAVITY;
      this.x += this.vx;
      this.y += this.vy;
      this.age++;
      this.alpha = Math.max(0, 1 - this.age / this.life);
    }
    draw(ctx) {
      // 发光渐变
      const g = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, this.size*6);
      g.addColorStop(0, `hsla(${this.hue},100%,70%,${this.alpha})`);
      g.addColorStop(0.2, `hsla(${this.hue},100%,55%,${this.alpha*0.8})`);
      g.addColorStop(1, `rgba(0,0,0,0)`);
      ctx.beginPath();
      ctx.fillStyle = g;
      ctx.globalCompositeOperation = 'lighter';
      ctx.arc(this.x, this.y, this.size*3, 0, Math.PI*2);
      ctx.fill();

      ctx.beginPath();
      ctx.globalAlpha = this.alpha;
      ctx.fillStyle = `hsl(${this.hue},100%,60%)`;
      ctx.arc(this.x, this.y, this.size, 0, Math.PI*2);
      ctx.fill();
      ctx.globalCompositeOperation = 'source-over';
    }
  }

  let rockets = [];
  let particles = [];

  function explode(x, y, hue) {
    const count = randInt(30, 140);
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = rand(1, 6) * (0.6 + Math.random()*0.9);
      const vx = Math.cos(angle) * speed;
      const vy = Math.sin(angle) * speed;
      const size = rand(1.2, 3.2);
      const life = randInt(40, 110);
      if (particles.length < MAX_PARTICLES) {
        particles.push(new Particle(x, y, vx, vy, (hue + rand(-30,30))|0, size, life));
      }
    }
    // 少量火花再次短时爆炸（二次爆）
    if (Math.random() < 0.06) {
      setTimeout(() => explode(x + rand(-10,10), y + rand(-10,10), hue+rand(-20,20)), randInt(100,250));
    }
  }

  // 自动生成火箭
  let autoTimer = setInterval(() => {
    launchRandom();
  }, AUTO_INTERVAL);

  function launchRandom() {
    const x = rand(W*0.1, W*0.9);
    const y = H + 10;
    const vx = rand(-1.2, 1.2);
    const vy = rand(-8.5, -11.5);
    const hue = randInt(0, 360);
    rockets.push(new Rocket(x, y, vx, vy, hue));
  }

  // 鼠标/触摸触发
  function spawnAt(clientX, clientY) {
    // 从下方向触发一个火箭，目标大致向点击点
    const x = rand(Math.max(50, clientX - 40), Math.min(W-50, clientX + 40));
    const y = H + 10;
    const vx = (clientX - x) * 0.02 + rand(-0.5,0.5);
    const vy = rand(-9.5, -13.5);
    const hue = randInt(0, 360);
    rockets.push(new Rocket(x, y, vx, vy, hue));
  }

  // 点击或触摸
  window.addEventListener('pointerdown', (e) => {
    spawnAt(e.clientX, e.clientY);
  });

  // 主循环
  function loop() {
    // 半透明覆盖以形成拖尾效果
    ctx.fillStyle = 'rgba(0,0,0,0.25)';
    ctx.fillRect(0, 0, W, H);

    // 背景微光点（少量随机星点）
    if (Math.random() < 0.04) {
      const sx = rand(0, W), sy = rand(0, H*0.6);
      ctx.beginPath();
      ctx.fillStyle = `rgba(255,255,255,${rand(0.02,0.12)})`;
      ctx.arc(sx, sy, rand(0.3,1.4), 0, Math.PI*2);
      ctx.fill();
    }

    // 更新 rockets
    for (let i = rockets.length-1; i >= 0; i--) {
      const r = rockets[i];
      r.update();
      r.draw(ctx);
      if (r.exploded) rockets.splice(i, 1);
    }

    // 更新 particles
    for (let i = particles.length -1; i >=0; i--) {
      const p = particles[i];
      p.update();
      p.draw(ctx);
      if (p.age > p.life || p.alpha <= 0.01 || p.y > H + 50) {
        particles.splice(i,1);
      }
    }

    requestAnimationFrame(loop);
  }

  // 启动
  loop();

  // 初始几个烟花
  for (let i=0;i<3;i++) setTimeout(launchRandom, i*300);

  // 性能保护（当 resize 或内存紧张时可清理）
  window.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      // 暂停自动发射，保留画面
      clearInterval(autoTimer);
    } else {
      // 恢复
      clearInterval(autoTimer);
      autoTimer = setInterval(() => launchRandom(), AUTO_INTERVAL);
    }
  });

})();
</script>
</body>
</html>
